#ifndef Networking_TCPClient_H
#define Networking_TCPClient_H

#include <stdio.h>
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <errno.h>
#define BufferSize 2048

#include "Networking_Exceptions.h"

namespace Networking
{
  class TCPClient
  {
    int FileDescriptor;
    int PortNumber;
    struct sockaddr_in ServerAddress;
    std::string ServerAddressString;
    std::string ServerIPString;
    char buffer[BufferSize];
    bool SocketInitialized;

    void CreateSocket(int Domain, int Type, int Protocol)
    {
      this->ServerIPString = NameResolve(ServerAddressString);
      this->FileDescriptor = socket(Domain, Type, Protocol);
      
      if (FileDescriptor < 0)
      {
	throw NetworkingException(errno, strerror(errno));
// 	switch (errno)
// 	{
// 	  case EACCES:
// 	    throw(new NetworkingException(errno,"Permission to create a socket of the specified type and/or protocol has been denied."));
// 	    break;
// 	  case EAFNOSUPPORT:
// 	    throw(new NetworkingException(errno,"The implementation does not support the specified address family."));
// 	    break;
// 	  case EINVAL:
// 	    throw(new NetworkingException(errno,"Unknown protocol, or protocol family not available, or invalid Type flag."));
// 	    break;
// 	  case EMFILE:
// 	    throw(new NetworkingException(errno,"Process file table overflow."));
// 	    break;
// 	  case ENFILE:
// 	    throw(new NetworkingException(errno,"The system limit on the total number of open files has been reached."));
// 	    break;
// 	  case ENOBUFS:
// 	    throw(new NetworkingException(errno,"Insufficient memory is available. The socket cannot be created until sufficient resources are freed."));
// 	    break;
// 	  case EPROTONOSUPPORT:
// 	    throw(new NetworkingException(errno,"The protocol type or the specified protocol is not supported within this domain."));
// 	    break;
// 	  default:
// 	    throw(new NetworkingException(errno,"Error creating socket."));
// 	    break;
// 	}
      }
      SocketInitialized = true;
    }
    
  public:
    TCPClient()
    {
      
    }
    
    TCPClient(std::string addr, int pn)
    {
      ServerAddressString = addr;
      PortNumber = pn;
      SocketInitialized = false;
    }
    
    TCPClient(std::string addr, int pn, int buffsize)
    {
      ServerAddressString = addr;
      PortNumber = pn;
      SocketInitialized = false;
    }
    
    ~TCPClient()
    {
      this->Close();
    }
    
    std::string SocketAddressASCII()
    {
      // ? what's this?
    }
    
    static std::string NameResolve(const std::string &hostname)
    {
      std::string ip;
      struct addrinfo hints, *res;
      struct in_addr addr;
      int err;

      memset(&hints, 0, sizeof(hints));
      hints.ai_socktype = SOCK_STREAM;
      hints.ai_family = AF_INET;

      getaddrinfo(hostname.c_str(), NULL, &hints, &res);

      addr.s_addr = reinterpret_cast<struct sockaddr_in *>(res->ai_addr)->sin_addr.s_addr;

      ip = inet_ntoa(addr);

      freeaddrinfo(res);

      return ip;
    }
    
    void Close()
    {
      close(FileDescriptor);
    }
    
    void Connect()
    {
      if (!SocketInitialized)
	CreateSocket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
      
      memset(&ServerAddress, 0, sizeof(ServerAddress));       /* Clear struct */
      ServerAddress.sin_family = AF_INET;                  /* Internet/IP TODO: Add AF_INET6 for IPv6 */
      ServerAddress.sin_addr.s_addr = inet_addr(ServerIPString.c_str());  /* IP address */
      ServerAddress.sin_port = htons(PortNumber);       /* server port */
      /* Establish connection */
      int c = connect(FileDescriptor, (struct sockaddr *) &ServerAddress, sizeof(ServerAddress));
      if (c < 0)
      {
	if(errno != EINPROGRESS)
	  throw NetworkingException(errno, strerror(errno));
// 	switch(errno)
// 	{
// 	  case EACCES:
// 	    throw(new NetworkingException(errno,"For Unix domain sockets, which are identified by pathname: Write permission is denied on the socket file, or search permission is denied for one of the directories in the path prefix."));
// 	    break;
// 	  case EPERM:
// 	    throw(new NetworkingException(errno,"You tried to connect to a broadcast address without having the socket broadcast flag enabled or the connection request failed because of a local firewall rule."));
// 	    break;
// 	  case EADDRINUSE:
// 	    throw(new NetworkingException(errno,"Local address is already in use."));
// 	    break;
// 	  case EAFNOSUPPORT:
// 	    throw(new NetworkingException(errno,"The passed address didn't have the correct address family in its sa_family field."));
// 	    break;
// 	  case EALREADY:
// 	    throw(new NetworkingException(errno,"The socket is nonblocking and a previous connection attempt has not yet been completed."));
// 	    break;
// 	  case EBADF:
// 	    throw(new NetworkingException(errno,"The file descriptor is not a valid index in the descriptor table."));
// 	    break;
// 	  case ECONNREFUSED:
// 	    throw(new NetworkingException(errno,"The connection has been refused. No-one listening on the remote address."));
// 	    break;
// 	  case EINTR:
// 	    throw(new NetworkingException(errno,"The system call was interrupted by a signal that was caught."));
// 	    break;
// 	  case EISCONN:
// 	    throw(new NetworkingException(errno,"This socket is already connected."));
// 	    break;
// 	  case ENETUNREACH:
// 	    throw(new NetworkingException(errno,"The network is unreachable."));
// 	    break;
// 	  case ENOTSOCK:
// 	    throw(new NetworkingException(errno,"The file descriptor is not associated with a socket."));
// 	    break;
// 	  case ETIMEDOUT:
// 	    throw(new NetworkingException(errno,"Timeout while attempting connection. The server may be too busy to accept new connections. Note that for IP sockets the timeout may be very long when syncookies are enabled on the server."));
// 	    break;
// 	}
      }
    }
    
    void Connect(const std::string &addr, int pn)
    {
      // TODO: Connect to server that is specified here.
    }


    void Process()
    {
      // TODO: Handle the multiplexer
    }

    bool SetNonBlocking()
    {
      int flags = fcntl(FileDescriptor, F_GETFL, 0);
      return !fcntl(FileDescriptor, F_SETFL, flags | O_NONBLOCK);
    }
    
    bool SetBlocking()
    {
      int flags = fcntl(FileDescriptor, F_GETFL, 0);
      return !fcntl(FileDescriptor, F_SETFL, flags & ~O_NONBLOCK);
    }
    
    bool Send(const std::string &msg)
    {
      size_t bytessent = send(FileDescriptor, msg.c_str(), msg.length(), 0);
      if(bytessent < 0)
	return false;
      return true;
    }
    
    bool Receive()
    {
      memset(buffer, 0, sizeof(buffer));

      size_t recvbytes = recv(FileDescriptor, buffer, BufferSize-1, 0);
      if (recvbytes < 0)
      {
	printf("Failed to receive bytes from server\n");
	return false;
      }
      printf("%s\n",buffer);
      return true;
    }

  };
}
#endif